/* **********************************************************
 * Copyright 2010 Dimitar Vangelovski
 *
 * This file is part of the Gens3 3D Engine.
 * 
 * The Gens3 3D Engine is free software; you can redistribute it and/or
 * modify it under the terms of version 2 of the GNU General Public
 * License as published by the Free Software Foundation.
 * 
 * The Gens3 3D Engine is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with the Gens3 3D Engine; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301, USA
 * **********************************************************/


#include "Camera.h"
#include "App.h"
#include "Vertex.h"
#include "GeomObject.h"
#include "Mesh.h"
#include "Scene.h"

// some camera parameters
// FIXME: this should be put in a config file!

const float Player_Sphere_Radius = 2.5f;
const float FreeFall_Gravity = 9.81f;
/*const*/ float Gravity_on_Ground = 2.0f;
const float Adjust_Gravity = 0.1f;
const float PiHalf = PI / 2 - 0.01f;
const float fMouseSensitivity = 2.0f;
const float fMspeed = 0.001f;
const bool m_filter = true;

const float kSpeed = 15.0f;
float currentRotX = 0.0f;
static bool bNoClip = false;
static bool bInvertMouse = false;
static bool bNoCl, bInvm;

//////////////////////////////////////////////////////////////////////////////////////////////////

CCamera::CCamera()
{
	m_vPosition	= CVector(0.0f, 0.0f, 0.0f);
	m_vView		= CVector(0.0f, 1.0f, 0.5f);
	m_vUpVector	= CVector(0.0f, 1.0f, 0.0f);
	m_fRadius = Player_Sphere_Radius;
}

CCamera::~CCamera()
{
}

// Set the camera view by mouse
void CCamera::MouseLook(SDL_Surface * pSurface)
{
	//POINT mousePos;
	int mousePosX = 0;
	int mousePosY = 0;
	int middleX = pSurface->w / 2;
	int middleY = pSurface->h / 2;
	float angleY = 0.0f;
	float angleX = 0.0f;
	float mousedx, mousedy, mouse_x, mouse_y;
	static float old_mouse_x = 0.0f;
	static float old_mouse_y = 0.0f;

	// Get the mouse's current X,Y position
	//::GetCursorPos(&mousePos);
	SDL_GetMouseState(&mousePosX, &mousePosY);
	
	// If our cursor is still in the middle, we never moved... so don't update the screen
	if ((mousePosX == middleX) && (mousePosY == middleY))
		return;

	// Set the mouse position to the middle of our window
	//::SetCursorPos(middleX, middleY);
	SDL_WarpMouse((Uint16)middleX, (Uint16)middleY);

	mousedx = (float)(middleX - mousePosX);
	mousedy = (float)(middleY - mousePosY);

	if (m_filter)
	{
		mouse_x = (mousedx + old_mouse_x) * 0.5f;
		mouse_y = (mousedy + old_mouse_y) * 0.5f;
	}
	else
	{
		mouse_x = mousedx;
		mouse_y = mousedy;
	}

	old_mouse_x = mousedx;
	old_mouse_y = mousedy;

	mouse_x *= fMouseSensitivity;
	mouse_y *= fMouseSensitivity;

	angleY = mouse_x * fMspeed;
	if (bInvertMouse)
		angleX = -mouse_y * fMspeed;
	else
		angleX = mouse_y * fMspeed;

	// Adjust the value of angleX to meet the limits of currentRotX
	if ((currentRotX - angleX) > PiHalf)
		angleX = currentRotX - PiHalf;
	if ((currentRotX - angleX) < -PiHalf) 
		angleX = currentRotX + PiHalf;

	// Keep track of the current X rotation to restrict vertical viewing
	currentRotX -= angleX;  
	
	// Restrict vertical viewing to +- 90 degrees
	if (currentRotX > PiHalf)
	{
		currentRotX = PiHalf;
	}
	else if (currentRotX < -PiHalf)
	{
		currentRotX = -PiHalf;
	}
	else
	{
		// Rotate around our perpendicular axis (side axis)
		RotateView(angleX, m_vStrafe.x, m_vStrafe.y, m_vStrafe.z);
	}

	// Rotate around our vertical axis
	RotateView(angleY, m_vUpVector.x, m_vUpVector.y, m_vUpVector.z);
}


// Rotate the view around the current position using angle and an axis
void CCamera::RotateView(float angle, float x, float y, float z)
{
	float fTemp1, fTemp2;

	float cx = (float)cos(angle * x);
	float sx = (float)sin(angle * x);
	float cy = (float)cos(angle * y);
	float sy = (float)sin(angle * y);
	float cz = (float)cos(angle * z);
	float sz = (float)sin(angle * z);

	CVector vNewView = m_vView - m_vPosition; // assign current view vector to calculate the rotation

	fTemp1 = vNewView.y * cx - vNewView.z * sx;
	fTemp2 = vNewView.z * cx + vNewView.y * sx;
	vNewView.y = fTemp1;
	vNewView.z = fTemp2;

	fTemp1 = vNewView.z * cy - vNewView.x * sy;
	fTemp2 = vNewView.x * cy + vNewView.z * sy;
	vNewView.z = fTemp1;
	vNewView.x = fTemp2;

	fTemp1 = vNewView.x * cz - vNewView.y * sz;
	fTemp2 = vNewView.y * cz + vNewView.x * sz;
	vNewView.x = fTemp1;
	vNewView.y = fTemp2;

	m_vView = m_vPosition + vNewView;
}

// move the camera on the sides (left - right)
void CCamera::StrafeCamera(float speed)
{	
	// Add the strafe vector to our position
	m_vPosition.x += m_vStrafe.x * speed;
	m_vPosition.z += m_vStrafe.z * speed;

	// Add the strafe vector to our view
	m_vView.x += m_vStrafe.x * speed;
	m_vView.z += m_vStrafe.z * speed;
}

// move the camera forward - backward
void CCamera::MoveCamera(float speed)
{
	CVector vMove;

	// Calculate the moving vector
	if (bNoClip)
	{
		// This is for moving in the direction we are looking (when gravity is disabled)
		vMove = m_vView - m_vPosition;
	}
	else
	{
		// This calculates a vector for constant horizontal moving speed, regardless
		// of the viewing direction (to enable moving while looking straight up or down)
		vMove = Cross(m_vUpVector, m_vStrafe);
	}
	vMove = Normalize(vMove);

	// Apply the moving vector to the position and view vectors
	m_vPosition.x += vMove.x * speed;
	m_vPosition.y += vMove.y * speed;
	m_vPosition.z += vMove.z * speed;

	m_vView.x += vMove.x * speed;
	m_vView.y += vMove.y * speed;
	m_vView.z += vMove.z * speed;
}

// check the camera (player) for collision against the world
void CCamera::CheckCameraCollision(CScene * pScene, float fFrameInterval)
{	
	if (bNoClip) return; // walk through walls

	static float    GlobalTimer = 0.0f;
	bool            Player_Standing_on_Ground = false;
	float           Gravity = 0.0f;

	// the edge collision bounce vectors
	CVector 	vEdgeNormal, vEdgeBounce;

	CMesh *         pMesh;
	CGeomObject *	pGeomObject;

	CMeshFace	meshFace;
	CVertex		meshVertex0, meshVertex1, meshVertex2;
	int         vn0, vn1, vn2;

	unsigned int 	iGeomObjCount = pScene->GetGeomObjectCount();
	int             iMeshNumFaces = 0;

	// go through all the triangles in the world
	for (unsigned int i = 0; i < iGeomObjCount; i++)
	{
		pGeomObject = pScene->GetGeomObject(i);
		pMesh = pGeomObject->GetMesh();
		iMeshNumFaces = pMesh->GetMeshNumFaces();

		for (int j = 0; j < iMeshNumFaces; j++)
		{
			meshFace = pMesh->GetMeshFace(j);
			
			vn0 = meshFace.GetVertexIndex(0);
			vn1 = meshFace.GetVertexIndex(1);
			vn2 = meshFace.GetVertexIndex(2);

			meshVertex0 = pMesh->GetVertex(vn0);
			meshVertex1 = pMesh->GetVertex(vn1);
			meshVertex2 = pMesh->GetVertex(vn2);
	
			// the current triangle we are testing collision with
			CVector vTriangle[3] = { 
					CVector(meshVertex0.x, meshVertex0.y, meshVertex0.z),
					CVector(meshVertex1.x, meshVertex1.y, meshVertex1.z),
					CVector(meshVertex2.x, meshVertex2.y, meshVertex2.z) };

			// calculate the normal of the triangle
			CVector vNormal = Normal(vTriangle);

			// distance of the collision sphere from the plane
			float fDistance = 0.0f;

			// determine if the sphere is in FRONT, BEHIND, or INTERSECTS the plane
			int classification = ClassifySphere(m_vPosition, vNormal, vTriangle[0], m_fRadius, fDistance);

			// check the intersection
			if (classification == INTERSECTS) 
			{
				// project the sphere center onto the triangle plane
				CVector vOffset = vNormal * fDistance;

				// calculate the intersection point
				CVector vIntersection = m_vPosition - vOffset;

				// check if the intersection point is inside the triangle
				// or it collides with the triangle edge

				//if (InsidePolygon(vIntersection, vTriangle, 3))
				if (InsideTriangle(vIntersection, vTriangle))
				{
					// check if the player is standing on ground and adjust the gravity
					// also limit the maximum steepness the player can climb
					if (Check_if_on_Ground(vNormal))
					{
						Player_Standing_on_Ground = true;
						if (pGeomObject->IsAnimated())
							Gravity_on_Ground = 0.0f;
						else
							Gravity_on_Ground = 2.0f;
					}

					// calculate the offset vector so we can move the sphere out of the collision
					vOffset = GetCollisionOffset(vNormal, m_fRadius, fDistance);

					// adjust the sphere position (player position) and view
					m_vPosition = m_vPosition + vOffset;
					m_vView = m_vView + vOffset;
				}
				else if (EdgeSphereCollision(m_vPosition, vTriangle, 3, m_fRadius, vEdgeBounce, vEdgeNormal))
				{
					// check if the player is standing on ground and adjust the gravity
					// also limit the maximum steepness the player can climb
					if (Check_if_on_Ground(vNormal))
					{
						Player_Standing_on_Ground = true;
						if (pGeomObject->IsAnimated())
							Gravity_on_Ground = 0.0f;
						else
							Gravity_on_Ground = 2.0f;
					}

					// calculate the offset vector so we can move the sphere out of the collision
					//vOffset = GetCollisionOffset(vNormal, m_fRadius, fDistance);
					vOffset = vEdgeBounce;

					// adjust the sphere position (player position) and view
					m_vPosition = m_vPosition + vOffset;
					m_vView = m_vView + vOffset;
				}
			}
		}
	}

	// add the gravity vector to the sphere
	if (!bNoClip)
	{
		if (Player_Standing_on_Ground)
		{
			GlobalTimer = 0.0015f;
			Gravity = Gravity_on_Ground;
		}
		else
		{
			GlobalTimer += fFrameInterval * Adjust_Gravity;
			Gravity = FreeFall_Gravity;
		}

		m_vPosition.y -= (Gravity * GlobalTimer);
		m_vView.y -= (Gravity * GlobalTimer);
	}
}

// check for keyboard presses and call the appropriate function for camera movement
void CCamera::CheckForMovement(Uint8 * pKeys, float fFrameInterval)
{	
	if (!pKeys) return;

	// Once we have the frame interval, we find the current speed
	float speed = kSpeed * fFrameInterval;
	//float speed = kSpeed * 0.001f;

	// Check if we hit the Up arrow or the 'w' key
	if (pKeys[SDLK_UP] || pKeys[SDLK_w]) 
	{
		MoveCamera(speed);				
	}

	// Check if we hit the Down arrow or the 's' key
	if (pKeys[SDLK_DOWN] || pKeys[SDLK_s]) 
	{
		MoveCamera(-speed);				
	}

	// Check if we hit the Left arrow or the 'a' key
	if (pKeys[SDLK_LEFT] || pKeys[SDLK_a]) 
	{
		StrafeCamera(-speed);
	}

	// Check if we hit the Right arrow or the 'd' key
	if (pKeys[SDLK_RIGHT] || pKeys[SDLK_d]) 
	{
		StrafeCamera(speed);
	}	

	// Jumping Tweak (jump on SPACE or right mouse button)
	if (pKeys[SDLK_SPACE] || (SDL_GetMouseState(NULL, NULL) & SDL_BUTTON(3))) 
	{
		m_vPosition.y += 0.33f;
		m_vView.y += 0.33f;
	}	

	// Toggle No_Clip
	if (pKeys[SDLK_F8] && bNoCl)
	{
		if (bNoClip)
		{
			bNoClip = false;
		}
		else
		{
			bNoClip = true;
		}
	}

	if (!(pKeys[SDLK_F8]))
	{
		bNoCl = true;
	}
	else
	{
		bNoCl = false;
	}

	// Toggle Invert Mouse
	if (pKeys[SDLK_F5] && bInvm)
	{
		if (bInvertMouse)
		{
			bInvertMouse = false;
		}
		else
		{
			bInvertMouse = true;
		}
	}

	if (!(pKeys[SDLK_F5]))
	{
		bInvm = true;
	}
	else
	{
		bInvm = false;
	}
}

// update the camera view and position
void CCamera::Update(SDL_Surface * pSurface, Uint8 * pKeys, float fFrameInterval) 
{
	// Calculate a perpendicular side vector used for side movement (strafing)
	CVector vSide = Cross(m_vView - m_vPosition, m_vUpVector);

	// Normalize the strafe vector
	m_vStrafe = Normalize(vSide);

	// Move the camera view by the mouse
	MouseLook(pSurface);

	// This checks to see if the keyboard was pressed
	CheckForMovement(pKeys, fFrameInterval);
	
	// Calculate our frame rate and set our frame interval for time-based movement
	//CalculateFrameRate();
}

// update the world camera position
void CCamera::Look()
{
	gluLookAt(m_vPosition.x, m_vPosition.y, m_vPosition.z,	
			  m_vView.x,	 m_vView.y,     m_vView.z,	
			  m_vUpVector.x, m_vUpVector.y, m_vUpVector.z);
}
